/**
 * Created on 24.10.2004
 * 
 */
package robocup.component;

import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import robocup.component.actionfactories.ViewModeActionFactory;
import robocup.component.actions.Action;
import robocup.component.actions.ActionSet;
import robocup.component.actions.ChangeViewModeAction;
import robocup.component.infotypes.AuralInfo;
import robocup.component.infotypes.SenseBodyInfo;
import robocup.component.infotypes.VisualInfo;
import robocup.component.statistics.HoleStatistic;
import robocup.component.statistics.InfoStatistic;
import robocup.component.tactics.MyStateEval;
import robocup.connection.TeamSoccerScopeMediator;

/**
 * This class synchronizes the client with the server.
 * 
 * As we are using a viewmode logic of 2x "narrow (45 deg.)" followed by 1x
 * "medium (90 deg.)" the visual infos (VIs) should arrive in steps of 75ms,
 * 75ms and 150ms(mdeium). As for the better quality we would like to have a
 * visual info each cycle (100ms) before determining an action. Therefore we
 * will change the viewmode to "medium" only if the last VI arrived in the first
 * quarter of an cycle and "narrow" when in the 2nd or 3rd quarter. (We could
 * easily achive the goal of 1 VI each cycle by using only the "narrow"
 * viewmode, but as this would result in 4 VIs in 3 Cycles and as the 2nd VI in
 * a cycle contains no new information we would loose 1 VI or ,in other words,
 * we would end up with 45 degrees less seen in 300ms.)
 * 
 * Due to noise, network latency and the delay for updating the world model and
 * calculating the corresponding action a VI arrival in the 4th quarter is not
 * diserable.
 * 
 * A perfect frequenzy would therefor look like this:
 * 
 * VI Arrival Quarter: 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 ....
 * New Viewmode : m n n m n n m n n m .... (m=medium, n=narrow)
 * 
 * The client always tries to wait for the VI before calculating an action.
 * However, due to fact that the calculation of some actions (e.g. passing,
 * scoring, dribbling,..) might take some time. To avoid the resulting holes we
 * calculate a preliminary (and lower quality) action based only on the body
 * sense info (BS-info). If not overwritten by a better action based on a VI
 * this action is send to the server a short time before the end of a cycle. As
 * this problem nearly only occurs for late VI, a preliminary action is only
 * calculated when we expect the VI to arrive in the 3rd quarter thus saving
 * calculating capacity that may be used by other players. The rate of sending
 * based only on a BS-Info normally remains below 1%.
 * 
 * @author Maximilian Kern, Robert Wetzker
 * 
 * TODO
 *  - include wide view mode (180 degrees) - use viewmode (narrow + quality low)
 * for moving the VI arrival offset by +-12.5 ms. (the next VI should arrive
 * after 37.5 ms. 37.5 mod 25 = 12.5ms)
 * 
 * i.e.
 * 
 * VI-Arrivals: 20ms -> medium (+150ms) -> 70ms -> narrow (+75ms)-> 45ms ->
 * narrow -> 20ms ....
 * 
 * With Correction:
 * 
 * 20ms -> narrow + low (+37.5ms) -> 57.5ms -> medium -> 7.5ms -> medium ->
 * 57.5ms -> narrow -> 32.5ms -> narrow -> 7.5ms ...
 * 
 * The max offset changing from 70ms to 57.5ms adding 12.5 ms for better action
 * calculation.
 * 
 * (HAS TO BE TESTED YET!!!)
 * 
 * 
 */
public class Synchro {

  // the parent
  private RobocupAgent         parent;

  // arrival time in ns of current SenseBodyInfo
  private long                 sbArrival            = -2;

  // actions were already sent this cycle
  private boolean              sent                 = false;

  // arrivaltime_vi - arrivaltime_bs
  private double               lastVIArrivalOffset  = -1;

  // the arrival quarter of the last VI (in an ideal world: 0 -> 0-25ms, 1 ->
  // 25-50ms .....)
  private int                  viArrivalQuarter     = 3;

  // the expected arrival quarter of the next VI (in an ideal world: 0 ->
  // 0-25ms, 1 -> 25-50ms .....)
  private int                  nextVIArrivalQuarter = 3;

  // cycle length of last info (not used yet) rw
  private long                 lastCycleLength      = 100;

  // the VI is the second VI in this cycle
  private boolean              is2ndVI              = false;

  // no VI arrived in the last cycle
  private boolean              lastCycleWasEmpty    = false;

  // action should be calculated after a BS because we don't expect a VI
  // or the VI arrival is unsure
  private boolean              calculateBSAction    = true;

  // the timer for the sending after an BS
  Timer                        bsTimer              = new Timer();

  // Statistics
  private final InfoStatistic        infoStatistic        = new InfoStatistic(this);

  private final HoleStatistic        holeStatistic        = new HoleStatistic(this);

  // print the Info-statistic every 100cycles
  private final boolean              SHOW_STATISTIC       = true;

  private final boolean              DEBUG                = false;

  // the actions that will be send
  StringBuffer                 actionBuffer              = new StringBuffer();

  // change view mode action was sent
  private boolean              viewModeSent;

  //the actions to send
  private ActionSet actions = new ActionSet();
  
  //default sending time was reached
  private boolean shouldHurry;

  /**
   * create a new synchro - this object should predict the times, when it is
   * necessary to send actions
   * 
   * @param parent -
   *          the agent object
   */
  public Synchro(
      final RobocupAgent parent) {

    this.parent = parent;
  }

  /**
   * This is called every new cycle resetting all actions and some flags.
   */
  private void reset() {

    this.sent = false;
    this.viewModeSent = false;
    this.actions = null;
    
    this.is2ndVI = false;
    this.shouldHurry = false;
  }

  /**
   * Called out of PlayerBean when a body sense info was parsed
   */
  public synchronized void arrivedBS(final SenseBodyInfo info) {
    
    // cancel timer -> no more sending from last cycle
    this.bsTimer.cancel();
    ((MyStateEval)this.getWorld().getSelfRef().getMyStateEval()).stop();
    
    // update hole statistic
    this.holeStatistic.update(info);

    // update gui hole statistic
    if (this.holeStatistic.isHole_main() 
        || this.holeStatistic.isHole_neck() 
        || this.holeStatistic.isHole_view()) {
      if (this.parent.useGui()) {
        TeamSoccerScopeMediator.registerHole(this.parent, 1);
      }
      if (this.getWorld().getNumber() == 10 && this.DEBUG) {
        System.out
            .println(this.getWorld().getCycleAndPlayerString() + "Hole in Last Cycle");
      }
    }

    // counter for last cycle VIs
    int i = 0;

    // set flag if no vi arrived in last cycle
    if (this.getWorld().getLongTermMemory().getVIArrivalTimes().length() > 0 && this
        .getWorld().getLongTermMemory().getBSArrivalTimes().length() > 0 && this
        .getWorld().getLongTermMemory().getLastVIArrivalTime() < this
        .getWorld().getLongTermMemory().getLastBSArrivalTime()) {
      this.lastCycleWasEmpty = true;
    }
    else {
      this.lastCycleWasEmpty = false;
    }

    // calculate what quarter the last VI was in
    if (this.getWorld().getLongTermMemory().getBSArrivalTimes().length() > 0) {
      this.lastCycleLength = (info.getArrivalTime() - this.sbArrival) / 1000000;
      if (this.getWorld().getLongTermMemory().getVIArrivalTimes().length() > 0) {
        // 75 or 150ms?
        final double interval = 
        	// info.getViewQuality().getFactor() * 
        	info.getViewWidth().getFactor() * 200;

        final int shift = (short) interval / 25;

        this.nextVIArrivalQuarter = this.viArrivalQuarter + shift - 4;

        // Debug output
        if (this.getWorld().getLongTermMemory().getBSArrivalTimes().length() > 1 && this
            .getWorld().getLongTermMemory().getVIArrivalTimes().length() > 1 && this
            .getWorld().getNumber() == 10 && this.DEBUG) {
          final long sbTime = info.getArrivalTime() - this.sbArrival;
          i = 0;
          long lastViArrivalTime;
          long viToBS = 0;
          double viRel;
          System.out
              .print("SynchroVIArrivals - " + (this.getWorld().getCycle() - 1) 
                  + ". BS:" + Math.round((this.sbArrival * 0.000001) % 1000) 
                  + " BS-Length:" + (int) (sbTime * 0.000001) + "\t" 
                  + " ViArrivalQ:" + this.viArrivalQuarter 
                  + " Shift:" + shift 
                  + " nextVIDelta:" + this.nextVIArrivalQuarter + "\t");
          // for all VIs that arrived in last cycle...
          while (i < this.getWorld().getLongTermMemory().getVIArrivalTimes()
              .length()) {
            lastViArrivalTime = this.getWorld().getLongTermMemory()
                .getLastVIArrivalTime(i);
            i++;
            if (lastViArrivalTime < this.sbArrival) {
              break;
            }
            viToBS = lastViArrivalTime - this.sbArrival;
            viRel = (int) ((double) viToBS / sbTime * 100);
            System.out
                .print(i + ": " + (int) (viToBS * 0.000001) + "ms\t" + viRel + "%\t");
          }
          System.out.println();
        }
        // if the VI is expected in the third quarter (beginning with
        // 0!!)
        // we calculate actions after BS for security reasons
        if (this.nextVIArrivalQuarter > 1 && this.getWorld().canKick()) {
          this.calculateBSAction = true;
        }
        else {
          this.calculateBSAction = false;
        }
      }
      this.viArrivalQuarter = this.nextVIArrivalQuarter;

    }

    // sent actions
    // the actions that were sent and accepted by the SServer will be
    // used to update the WorldModel after an BS info
    final Vector<Action> lastSentActions = new Vector<Action>();
    if(this.actions != null){

      if (!this.holeStatistic.isHole_main() && this.actions.getMain() != null) {
        lastSentActions.add(this.actions.getMain());
      }
      if (!this.holeStatistic.isHole_neck() && this.actions.getNeck() != null) {
        lastSentActions.add(this.actions.getNeck());
      }
      if (!this.holeStatistic.isHole_view() && this.actions.getView() != null) {
        lastSentActions.add(this.actions.getView());
      }
      if (this.actions.getAttention() != null) {
        lastSentActions.add(this.actions.getAttention());
      }
      if (this.actions.getSay() != null) {
        lastSentActions.add(this.actions.getSay());
      }
      if (this.actions.getPointTo() != null) {
        lastSentActions.add(this.actions.getPointTo());
      }
    }

    info.setLastSentActions(lastSentActions);

    // set actions and flags to "null"
    this.reset();

    this.sbArrival = info.getArrivalTime();

    // statistics
    if (this.SHOW_STATISTIC) {
      this.infoStatistic.update(info);
    }
    if (this.SHOW_STATISTIC && this.getWorld().getCycle() % 1000 == 999) {
      this.infoStatistic.show();
      this.holeStatistic.show();
    }
    
    this.startTimerAfterBS();
  }

  /**
   * starts a waiting thread after a bs arrival. Once awake this thread will
   * force the sending of an action.
   *
   */
  private void startTimerAfterBS(){
    long minCycleLength;
    if (this.getWorld().getLongTermMemory().getCycleLength().length() > 0) {
      minCycleLength = this.getWorld().getLongTermMemory().getCycleLength()
          .getMin(5);
    }
    else {
      minCycleLength = SConf.getInstance().SENSE_BODY_STEP * 1000000;
    }

    long thinkTime = 5000000;

    // action calculation with the ball kickable is more time consuming
    if (this.getWorld().isInitialized() && this.getWorld().canKick()) {
      if (this.getWorld().getLongTermMemory().getThinkTimesWithBall()
          .length() > 0) {
        thinkTime = this.getWorld().getLongTermMemory()
            .getThinkTimesWithBall().getMax(5);
      }
    }
    else {
      if (this.getWorld().getLongTermMemory().getThinkTimes45().length() > 0) {
        thinkTime = this.getWorld().getLongTermMemory().getThinkTimes45()
            .getMax(5);
      }
    }

    // world update and action calculation with more seen objects
    // and therefor more information to consider might be more time consuming
    if (this.getWorld().getViewWidth() == 90.0) {
      thinkTime *= 1.5;
    }

    final long delay = (long) (minCycleLength + this.sbArrival - System
        .nanoTime()) / 1000000 - 15;

    if(delay > 0)
      this.setTimer(delay);
    else
      this.sendAll();
  }
  
  /**
   * Called out of PlayerBean when a visual info arrives
   */
  public synchronized void arrivedVI(final VisualInfo visualInfo) {

    if (this.getWorld().getNumber() == 10 && this.DEBUG) {
      System.out
          .println(((System.nanoTime() / 1000000) % 1000) + " ARRIVED VI");
    }

    // vi_arrival - bs_arrival
    if (this.getWorld().getLongTermMemory().getBSArrivalTimes().length() == 0) {
      this.lastVIArrivalOffset = 80;
    }
    else {
      this.lastVIArrivalOffset = (visualInfo.getArrivalTime() - this.getWorld()
          .getLongTermMemory().getLastBSArrivalTime()) / 1000000;
    }

    // is it the second VI that arrives in this cycle
    this.is2ndVI = (this.getWorld().getLongTermMemory().getVIArrivalTimes()
        .length() > 1) ? this.getWorld().getLongTermMemory()
        .getLastVIArrivalTime() > this.sbArrival : false;
    
    // no vi in last cycle -> this vi arrived at the beginning
    // TODO consider 300ms intervals
    if (this.lastCycleWasEmpty) {
      this.viArrivalQuarter = 0;
    }
    
    // vi arrived n 1st quarter of cycle
    if (this.lastVIArrivalOffset < 25) {
      this.viArrivalQuarter = 0;
    }
    // vi arrived in last quarter of cycle
    // VALID only if no 37.5ms modes are used!!!
    if (this.is2ndVI) {
      this.viArrivalQuarter = 3;
    }
    
    // statistics
    if (this.SHOW_STATISTIC) {
      this.infoStatistic.update(visualInfo);
    }
    
    //this.sendChangeViewModeAction();
  }
  
  /**
   * send only the {@link ChangeViewModeAction}
   *
   */
  @SuppressWarnings("unused")
  private void sendChangeViewModeAction(){
    if(this.viewModeSent)
      return;

    final ChangeViewModeAction a = ViewModeActionFactory.getAction(this);
    
    this.parent.sendActions(a.toString());
    this.viewModeSent = true;   
  }


  /**
   * Called out of PlayerBean when a aural info arrived
   */
  public synchronized void arrivedAural(final AuralInfo auralInfo) {

  }

  /**
   * send actions. if actions are based on a VI this method sends these actions
   * to the SoccerServer (via RobocupAgent). If based on BS a TimerTask is
   * started which will send the actions if no VI arrives.
   * 
   */
  public synchronized void send() {
    if(this.getWorld().isViBased()){
      this.sendAll();
    }
    
    if(true)
      return;
    // we got an action but it was based only on an BS info
    // so we wait as long as possible hoping a VI might arrive improving our
    // worldmodel
    // and leading to a better action.

    // the time we will wait is based on the expected duration of the cycle
    // (might differ
    // due to slow server and/or network delay) minus the expected time for
    // calculating an action
    // in the case a VI will arrive minus some threshold.
    if (this.calculateBSAction == true && !this.getWorld().isViBased()) {

      long minCycleLength;
      if (this.getWorld().getLongTermMemory().getCycleLength().length() > 0) {
        minCycleLength = this.getWorld().getLongTermMemory().getCycleLength()
            .getMin(5);
      }
      else {
        minCycleLength = SConf.getInstance().SENSE_BODY_STEP * 1000000;
      }

      long thinkTime = 5000000;

      // action calculation with the ball kickable is more time consuming
      if (this.getWorld().canKick()) {
        if (this.getWorld().getLongTermMemory().getThinkTimesWithBall()
            .length() > 0) {
          thinkTime = this.getWorld().getLongTermMemory()
              .getThinkTimesWithBall().getMax(5);
        }
      }
      else {
        if (this.getWorld().getLongTermMemory().getThinkTimes45().length() > 0) {
          thinkTime = this.getWorld().getLongTermMemory().getThinkTimes45()
              .getMax(5);
        }
      }

      // world update and action calculation with more seen objects
      // and therefor more information to consider might be more time consuming
      if (this.getWorld().getViewWidth() == 90.0) {
        thinkTime *= 1.5;
      }

      final long delay = (long) (minCycleLength - thinkTime + this.sbArrival - System
          .nanoTime()) / 1000000 - 20;

      if (this.getWorld().getNumber() == 10 && this.DEBUG) {
        System.out
            .println("Delay:" + delay + " Min:" + minCycleLength + " TT:" + thinkTime + "System:" + System
                .nanoTime() + "this.sbArrival" + this.sbArrival + (this
                .getWorld().canKick() ? " *** KICKING *** " : ""));
      }

      if (delay < 0) {
        this.sendAllButTurnNeckAndAttention();
      }
      else {
        this.setTimer(delay);
      }
    }
    // based on VI -> sendAll
    else {
      this.sendAll();
    }
  }

  /**
   * Sends all actions to be executed and stores some meta data.
   * 
   * All sent actions are remembered.
   */
  private synchronized void sendAll() {

    // cancel Timer
    this.bsTimer.cancel();

    if (this.sent) {
      return;
    }
    else{
     ((MyStateEval)this.getWorld().getSelfRef().getMyStateEval()).stop();
     if(this.actions == null)
       return;
    }

    // save thinking times
    if (this.getWorld().isViBased() && this.getWorld().getLongTermMemory()
        .getVIArrivalTimes().length() > 0) {
      final long thinkTime = System.nanoTime() - this.getWorld().getLongTermMemory()
          .getVIArrivalTimes().getLast();
      final double viewWidth = this.getWorld().getViewWidth();
      if (viewWidth == 45.0) {
        if (this.getWorld().canKick()) {
          this.getWorld().getLongTermMemory().getThinkTimesWithBall().insert(
              thinkTime);
        }
        else {
          this.getWorld().getLongTermMemory().getThinkTimes45().insert(
              thinkTime);
        }
      }
    }

    this.actionBuffer.setLength(0);
    
    this.actions.appendActions(this.actionBuffer, 
        true, true, !this.viewModeSent, true, true, true);

    if (this.actionBuffer.length() != 0) {
      // here everything is sent
      this.parent.sendActions(this.actionBuffer.toString());
    }
    this.sent = true;

    if (this.SHOW_STATISTIC) {
      this.infoStatistic.updateAfterSending();
    }
  }

  /**
   * Send all actions but the turn neck and the attentionto action to the
   * SServer.
   * 
   * This is used for sending based on a BS info where the last sent turn neck
   * action still had no effect on our world vision and the attentionTo action
   * had no effect as hear messages arrive after the BS info.
   */
  private synchronized void sendAllButTurnNeckAndAttention() {

    // cancel Timer
    this.bsTimer.cancel();

    if (this.sent || this.actions == null) {
      return;
    }
    else{
     ((MyStateEval)this.getWorld().getSelfRef().getMyStateEval()).stop();
    }
    
    // save thinking times
    if (this.getWorld().isViBased() && this.getWorld().getLongTermMemory()
        .getVIArrivalTimes().length() > 0) {
      final long thinkTime = System.nanoTime() - this.getWorld().getLongTermMemory()
          .getVIArrivalTimes().getLast();
      final double viewWidth = this.getWorld().getViewWidth();
      if (viewWidth == 45.0) {
        if (this.getWorld().canKick()) {
          this.getWorld().getLongTermMemory().getThinkTimesWithBall().insert(
              thinkTime);
        }
        else {
          this.getWorld().getLongTermMemory().getThinkTimes45().insert(
              thinkTime);
        }
      }
    }

    this.actionBuffer.setLength(0);
    this.actions.appendActions(this.actionBuffer, 
        true, false, !this.viewModeSent, true, true, true);

    if (this.actionBuffer.length() != 0) {
      // here everything is sent
      this.parent.sendActions(this.actionBuffer.toString());
    }
    this.sent = true;

    if (this.SHOW_STATISTIC) {
      this.infoStatistic.updateAfterSending();
    }
  }

  /**
   * the parent (PlayerBean)
   * 
   * @return
   */
  @SuppressWarnings("unused")
  private RobocupAgent getParent() {

    return this.parent;
  }

  /**
   * true if actions were sent in this cycle.
   * 
   * @return
   */
  public boolean wasSent() {

    return this.sent;
  }

  /**
   * @return Returns the lastVIArrivalOffset.
   */
  public double getLastVIArrivalOffset() {

    return this.lastVIArrivalOffset;
  }

  /**
   * @return Returns the lastCycleLength.
   */
  public long getLastCycleLength() {

    return this.lastCycleLength;
  }

  /**
   * @return Returns the viArrivalQuarter.
   */
  public int getVIArrivalQuarter() {

    return this.viArrivalQuarter;
  }

  /**
   * the percentage of holes for cycles where the player can kick the ball
   * 
   * @return
   */
  public double getWithBallHolePercentage() {

    if (this.holeStatistic.getCount_with_ball() == 0) {
      return 0;
    }

    return (double) this.holeStatistic.getHoles_with_ball() 
        / this.holeStatistic.getCount_with_ball();
  }

  /**
   * @return Returns the is2ndVI.
   */
  public boolean is2ndVI() {

    return this.is2ndVI;
  }

  /**
   * @return Returns the calculateBSAction.
   */
  public boolean calculateBSAction() {

    return this.calculateBSAction;
  }

  /**
   * Sets the TimerTask after the arrival of a BS.
   * 
   * @param offset
   */
  public void setTimer(final long offset) {

    this.bsTimer = new Timer();
    this.bsTimer.schedule(Synchro.invokeBSCycle(this), offset);
  }

  /**
   * simple TimerTask that sends after a certain waiting period.
   * 
   * @param synchro
   * @return
   */
  private static TimerTask invokeBSCycle(final Synchro synchro) {

    return new TimerTask() {

      @Override
      public void run() {
//        synchro.sendAllButTurnNeckAndAttention();
        synchro.sendAllNow();
      }
    };
  }

  private synchronized void sendAllNow(){
    if(this.wasSent())
      return;
    
    if(this.actions != null){
      if(this.getWorld().isViBased())
        this.sendAll();
      else
        this.sendAllButTurnNeckAndAttention();
    }
    else{
      ((MyStateEval) this.getWorld().getSelfRef()
          .getMyStateEval()).hurry();
      this.shouldHurry = true;
    }
  }
  
  /**
   * The WorldModel.
   * 
   * @return the WM.
   */
  public WorldModel getWorld() {

    return this.parent.getWorld();
  }
  
  /**
   * The actions.
   *
   * @return the actions
   */
  public ActionSet getActions() {
  
    return this.actions;
  }

  /**
   * @param actionSet
   */
  public void setActions(final ActionSet actions) {

    this.actions = actions;    
  }

  /**
  * true if it was already time for sending
  */
  public boolean isShouldHurry() {
	return false;
  }
}
